Black Friday Sale Upgrade Your Home →

Extracting Container Components

Extracting Container Components (FilterLink)

In the previous section, we separated presentational components from our main container component. TodoApp specifies the behaviors when buttons are clicked, items are added, and filters are applied. The individual presentational components, such as AddTodos, Footer, TodoList, etc don't dispatch actions, but instead call their callback functions in the props. Therefore, they are only responsible for the looks, not the behavior.

The downside of this approach is that lots of props must be passed down the tree even when intermediate components don't really use them.

For example, the FilterLink needs to know the current filter so it can change its appearance when it's active. However, in order for it to receive the current filter, it has to be passed down from the top. This is why Footer has to be given visibilityFilter so it can be passed to a FilterLink.

In a way this breaks encapsulation because the parent components need to know too much about what data the child components need. To fix this, we are going to extract some more container components.

Extracting the Footer Component

Currently the Footer component accepts the visibilityFilter and onFilterClick() callback as its props, but it doesn't actually use either of them. It just passes down to the FilterLink. We can only do this because we know that the Footer component doesn't care about the values of its props, as they only exist to pass down to FilterLink.

We start by removing the props definition from the Footer component, and removing them from the FilterLinks as well.

JAVASCRIPT
const Footer = () => (
<p>
Show:
{' '}
<FilterLink
filter='SHOW_ALL'
>
All
</FilterLink>
{', '}
<FilterLink
filter='SHOW_ACTIVE'
>
Active
</FilterLink>
{', '}
<FilterLink
filter='SHOW_COMPLETED'
>
Completed
</FilterLink>
</p>
)

Refactoring FilterLink

Inside of the FilterLink definition, we don't currently specify behavior for clicking on the link. It also needs to know the current filter so it can render the item appropriately. Because of this, we can't say FilterLink is presentational, because it is inseparable from its behavior. The only reasonable behavior is to dispatch an action (SET_VISIBILITY_FILTER) upon clicking. This is an opportunity to split this into a more concise presentational component, with a wrapping container component to manage the logic, with the presentational component being used for rendering.

Therefore, we will start by converting our current FilterLink into a presentational component called Link.

The new Link presentational component doesn't know anything about the filter-- it only accepts the active prop, and calls its onClick handler. Link is only concerned with rendering.

JAVASCRIPT
const Link = ({
active,
children,
onClick
}) => {
if (active) {
return <span>{children}</span>
}
return (
<a href='#'
onClick={e => {
e.preventDefault();
onClick();
}}
>
{children}
</a>
);
};

The New FilterLink

The new FilterLink will be a class that renders the Link with the current data from the store. It's going to read the component props and read the state. Note: this doesn't mean React's state, but instead the Redux store's state that it gets by calling store.getState().

As a container component, FilterLink doesn't have its own markup, and it delegates rendering to the Link presentational component. In this case, it calculates its active prop by comparing its own filter prop with the visibilityFilter in the Redux store's state.

The filter prop is the one that is passed to the FilterLink from the Footer. The visibilityFilter corresponds to the current chosen visibility filter that is held in Redux store's state. If they match, we want the link to appear active.

The container component also needs to specify the behavior. In this case, the FilterLink specifies that when a particular Link is clicked, we should dispatch an action of the type 'SET_VISIBILITY_FILTER' along with the filter value that we take from the props.

The FilterLink may accept children, which will be used as the contents of the Link. So we are going to pass the children down to the Link component, which is going to render them inside of the <a> tag.

JAVASCRIPT
class FilterLink extends Component {
render () {
const props = this.props;
// this just reads the store, is not listening
// for change messages from the store updating
const state = store.getState();
return (
<Link
active={
props.filter ===
state.visibilityFilter
}
onClick={() =>
store.dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: props.filter
})
}
>
{props.children}
</Link>
);
}
}

Problems with FilterLink

There is a small problem with this implementation of FilterLink. Inside the render() method it reads the current state of the Redux store, however it does not subscribe to the store. So if the parent component doesn't update when the store is updated, the correct value won't be rendered.

Also, we currently re-render the entire application when the state changes, which isn't very efficient. In the future, we will move subscription to the React lifecycle methods of the container components.

React provides a special forceUpdate() method on the Component instances to force them to re-render. We can use it in combination with the store.subscribe() method so that any time the store changes we force the container component to update.

We can start by implementing this inside FilterLink:

JAVASCRIPT
class FilterLink extends Component {
componentDidMount() {
this.unsubscribe = store.subscribe(() =>
this.forceUpdate()
);
}
// Since the subscription happens in `componentDidMount`,
// it's important to unsubscribe in `componentWillUnmount`.
componentWillUnmount() {
this.unsubscribe(); // return value of `store.subscribe()`
}
.
. // `render()` method as above...
.

Extracting Container Components (VisibleTodoList, AddTodo)

Now let's work with the TodoList component. We want to keep it as a presentational component, but we want to encapsulate reading the currently visible todos into a separate container component that connects the TodoList to the Redux store.

This component will be called VisibleTodoList. Just like when we created the FilterLink component, the data for VisibleTodoList will be calculated by using the current state. We will use the getVisibleTodos() function to go through all of the todos in the Redux store and determine which ones should be shown according to the visibilityFilter.

We also will specify the behavior of onTodoClick() to dispatch an action of type 'TOGGLE_TODO' along with the id.

The same subscription logic we used in our FilterLink component needs to be included as well.

JAVASCRIPT
class VisibleTodoList extends Component {
componentDidMount() {
this.unsubscribe = store.subscribe(() =>
this.forceUpdate()
);
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
const props = this.props;
const state = store.getState();
return (
<TodoList
todos={
getVisibleTodos(
state.todos,
state.visibilityFilter
)
}
onTodoClick={id =>
store.dispatch({
type: 'TOGGLE_TODO',
id
})
}
/>
);
}
}

Remember, the job of all container components is similar-- connect a presentational component to the Redux store, and specify the data and behavior that it needs.

Now we can replace TodoList in our TodoApp with our newly created VisibleTodoList.

Changing AddTodo to a Container

In the last section we made AddTodo into a presentational component. Now we will backtrack on this.

We start by moving the onClick handler from TodoApp into the AddTodo component. We're doing this because there isn't a lot of presentaion or behavior here, and it's easier to keep them together until we figure out how to split the presentation from the behavior. For example, in the future we may decide to have a Form component

JAVASCRIPT
const AddTodo = () => {
let input;
return (
<div>
<input ref={node => {
input = node;
}} />
<button onClick={() => {
store.dispatch({
type: 'ADD_TODO',
id: nextTodoId++,
text: input.value
})
input.value = '';
}}>
Add Todo
</button>
</div>
);
};

Refactored TodoApp

Now that we've refactored our components, it's become clear that none of the containers need props from TodoApp! We can also get rid of TodoApp's render() function that rendered the current state of the store.

We can get rid of the render() function because the container components inside of TodoApp are now set up to get state and update themselves as needed, therefore, we only need to render TodoApp once on initialization.

JAVASCRIPT
const TodoApp = () => (
<div>
<AddTodo />
<VisibleTodoList />
<Footer />
</div>
);
// Note this render does not belong to `TodoApp`
ReactDOM.render(
<TodoApp />,
document.getElementById('root')
);

  Previous      Next